/*-------------------------------------------------------------------------
 *
 * storage.c
 *	  创建和销毁关系的物理存储的代码
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/backend/catalog/storage.c
 *
 * NOTES
 *	  这部分代码以前在storage/smgr/smgr.c中，函数名称仍然反映了这一点。
 *
 *-------------------------------------------------------------------------
 */

#include "postgres.h"

#include "access/parallel.h"
#include "access/visibilitymap.h"
#include "access/xact.h"
#include "access/xlog.h"
#include "access/xloginsert.h"
#include "access/xlogutils.h"
#include "catalog/storage.h"
#include "catalog/storage_xlog.h"
#include "miscadmin.h"
#include "storage/freespace.h"
#include "storage/smgr.h"
#include "utils/hsearch.h"
#include "utils/memutils.h"
#include "utils/rel.h"

/* GUC 变量 */
int			wal_skip_threshold = 2048;	/* 以千字节为单位 */

/*
 * 我们保留一个当前事务中所有创建或删除的关系
 * （以 RelFileNode 值表示）的列表。当
 * 创建一个关系时，我们立即创建物理文件，
 * 但记住它，以便如果当前
 * 事务被中止时可以再次删除该文件。相反，删除请求不会
 * 立即执行，而只是在列表中输入。当事务提交时，可以
 * 删除物理文件。
 *
 * 为了处理子事务，每个条目都标记有其事务
 * 嵌套级别。在子事务提交时，我们将子事务的
 * 条目重新分配给父级嵌套级别。在子事务中止时，
 * 我们可以立即对当前
 * 嵌套级别的所有条目执行中止时操作。
 *
 * 注意：列表保存在 TopMemoryContext 中，以确保它不会消失
 * 而不及时。将其保存在 TopTransactionContext 中可能也可以，
 * 但我比较小心。
 */

typedef struct PendingRelDelete
{
	RelFileNode relnode;		/* 可能需要删除的关系 */
	BackendId	backend;		/* 如果不是临时关系，则为 InvalidBackendId */
	bool		atCommit;		/* T=在提交时删除；F=在中止时删除 */
	int			nestLevel;		/* 请求的事务嵌套级别 */
	struct PendingRelDelete *next;	/* 链表链接 */
} PendingRelDelete;

typedef struct PendingRelSync
{
	RelFileNode rnode;
	bool		is_truncated;	/* 文件是否经历了截断？ */
} PendingRelSync;

static PendingRelDelete *pendingDeletes = NULL; /* 链表头 */
static HTAB *pendingSyncHash = NULL;


/*
 * AddPendingSync
 *		在提交时队列 fsync。
 */
static void fc_AddPendingSync(const RelFileNode *fc_rnode)
{
	PendingRelSync *fc_pending;
	bool		fc_found;

	/* 如果尚未创建，则创建哈希 */
	if (!pendingSyncHash)
	{
		HASHCTL		fc_ctl;

		fc_ctl.keysize = sizeof(RelFileNode);
		fc_ctl.entrysize = sizeof(PendingRelSync);
		fc_ctl.hcxt = TopTransactionContext;
		pendingSyncHash = hash_create("pending sync hash", 16, &fc_ctl,
									  HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
	}

	fc_pending = hash_search(pendingSyncHash, fc_rnode, HASH_ENTER, &fc_found);
	Assert(!fc_found);
	fc_pending->is_truncated = false;
}

/*
 * RelationCreateStorage
 *		为关系创建物理存储。
 *
 * 为关系创建底层磁盘文件存储。这只
 * 创建主分叉；附加的分叉由
 * 需要它们的模块懒惰地创建。
 *
 * 此函数是事务性的。创建是 WAL 记录，
 * 如果事务稍后中止，存储将被销毁。一个希望在中止的情况下不
 * 要销毁存储的调用者可以传递 register_delete = false。
 */
SMgrRelation
RelationCreateStorage(RelFileNode fc_rnode, char fc_relpersistence,
					  bool fc_register_delete)
{
	SMgrRelation fc_srel;
	BackendId	fc_backend;
	bool		fc_needs_wal;

	Assert(!IsInParallelMode());	/* 无法更新 pendingSyncHash */

	switch (fc_relpersistence)
	{
		case RELPERSISTENCE_TEMP:
			fc_backend = BackendIdForTempRelations();
			fc_needs_wal = false;
			break;
		case RELPERSISTENCE_UNLOGGED:
			fc_backend = InvalidBackendId;
			fc_needs_wal = false;
			break;
		case RELPERSISTENCE_PERMANENT:
			fc_backend = InvalidBackendId;
			fc_needs_wal = true;
			break;
		default:
			elog(ERROR, "invalid relpersistence: %c", fc_relpersistence);
			return NULL;		/* 安抚编译器 */
	}

	fc_srel = smgropen(fc_rnode, fc_backend);
	smgrcreate(fc_srel, MAIN_FORKNUM, false);

	if (fc_needs_wal)
		log_smgrcreate(&fc_srel->smgr_rnode.node, MAIN_FORKNUM);

	/*
	 * 如果我们被要求这样做，则将关系添加到在中止时删除的列表中。
	 */
	if (fc_register_delete)
	{
		PendingRelDelete *fc_pending;

		fc_pending = (PendingRelDelete *)
			MemoryContextAlloc(TopMemoryContext, sizeof(PendingRelDelete));
		fc_pending->relnode = fc_rnode;
		fc_pending->backend = fc_backend;
		fc_pending->atCommit = false;	/* 如果中止则删除 */
		fc_pending->nestLevel = GetCurrentTransactionNestLevel();
		fc_pending->next = pendingDeletes;
		pendingDeletes = fc_pending;
	}

	if (fc_relpersistence == RELPERSISTENCE_PERMANENT && !XLogIsNeeded())
	{
		Assert(fc_backend == InvalidBackendId);
		fc_AddPendingSync(&fc_rnode);
	}

	return fc_srel;
}

/*
 * 执行 XLogInsert 的 XLOG_SMGR_CREATE 记录以写入 WAL。
 */
void log_smgrcreate(const RelFileNode *fc_rnode, ForkNumber fc_forkNum)
{
	xl_smgr_create fc_xlrec;

	/*
	 * 制作一条 XLOG 记录，报告文件创建。
	 */
	fc_xlrec.rnode = *fc_rnode;
	fc_xlrec.forkNum = fc_forkNum;

	XLogBeginInsert();
	XLogRegisterData((char *) &fc_xlrec, sizeof(fc_xlrec));
	XLogInsert(RM_SMGR_ID, XLOG_SMGR_CREATE | XLR_SPECIAL_REL_UPDATE);
}

/*
 * RelationDropStorage
 *		在事务提交时调度物理存储的解除链接。
 */
void RelationDropStorage(Relation fc_rel)
{
	PendingRelDelete *fc_pending;

	/* 将关系添加到在提交时删除的列表中 */
	fc_pending = (PendingRelDelete *)
		MemoryContextAlloc(TopMemoryContext, sizeof(PendingRelDelete));
	fc_pending->relnode = fc_rel->rd_node;
	fc_pending->backend = fc_rel->rd_backend;
	fc_pending->atCommit = true;	/* 如果提交则删除 */
	fc_pending->nestLevel = GetCurrentTransactionNestLevel();
	fc_pending->next = pendingDeletes;
	pendingDeletes = fc_pending;

	
/*
	 * 注意：如果关系是在此事务中创建的，则它现在将
	 * 在待删除列表中出现两次，一次为 atCommit true，另一次为 atCommit false。
	 * 因此，无论哪种情况，它将在 xact 结束时被物理删除（另一项条目将
	 * 被 smgrDoPendingDeletes 忽略，因此不会发生错误）。我们也可以
	 * 删除现有列表条目并立即删除物理文件，但目前我将保持逻辑简单。
	 */

	RelationCloseSmgr(fc_rel);
}

/*
 * RelationPreserveStorage
 *		将关系标记为最终不删除。
 *
 * 我们需要这个函数，因为关系映射的更改是与整个事务的提交分开提交的，
 * 因此在映射更新完成后，事务仍然可能会中止。
 * 当在映射中安装新的物理关系时，它将被安排为在中止时删除，
 * 所以我们会删除它，这将导致问题。
 * 关系映射器通过告诉我们最终不要删除这样的关系来解决这个问题，
 * 作为其提交的一部分。
 *
 * 我们还用这个来在 ALTER TABLE 过程中重用旧的索引构建，
 * 这次删除删除在提交时的条目。
 *
 * 如果关系不在计划删除的列表中，则不执行任何操作。
 */
void RelationPreserveStorage(RelFileNode fc_rnode, bool fc_atCommit)
{
	PendingRelDelete *fc_pending;
	PendingRelDelete *fc_prev;
	PendingRelDelete *fc_next;

	fc_prev = NULL;
	for (fc_pending = pendingDeletes; fc_pending != NULL; fc_pending = fc_next)
	{
		fc_next = fc_pending->next;
		if (RelFileNodeEquals(fc_rnode, fc_pending->relnode)
			&& fc_pending->atCommit == fc_atCommit)
		{
			/* 解除链接并删除列表条目 */
			if (fc_prev)
				fc_prev->next = fc_next;
			else
				pendingDeletes = fc_next;
			pfree(fc_pending);
			/* prev 不变 */
		}
		else
		{
			/* 无关条目，不要触碰它 */
			fc_prev = fc_pending;
		}
	}
}

/*
 * RelationTruncate
 *		将关系物理截断到指定的块数。
 *
 * 这包括清除要删除的块的任何缓冲区。
 */
void RelationTruncate(Relation fc_rel, BlockNumber fc_nblocks)
{
	bool		fc_fsm;
	bool		fc_vm;
	bool		fc_need_fsm_vacuum = false;
	ForkNumber	fc_forks[MAX_FORKNUM];
	BlockNumber fc_old_blocks[MAX_FORKNUM];
	BlockNumber fc_blocks[MAX_FORKNUM];
	int			fc_nforks = 0;
	SMgrRelation fc_reln;

	/*
	 * 确保 smgr_targblock 等不指向新结束后的某个地方。
	 * (注意：不要依赖此循环以下的 reln 指针。)
	 */
	fc_reln = RelationGetSmgr(fc_rel);
	fc_reln->smgr_targblock = InvalidBlockNumber;
	for (int fc_i = 0; fc_i <= MAX_FORKNUM; ++fc_i)
		fc_reln->smgr_cached_nblocks[fc_i] = InvalidBlockNumber;

	/* 为主关系的截断做准备 */
	fc_forks[fc_nforks] = MAIN_FORKNUM;
	fc_old_blocks[fc_nforks] = smgrnblocks(fc_reln, MAIN_FORKNUM);
	fc_blocks[fc_nforks] = fc_nblocks;
	fc_nforks++;

	/* 如果存在，为 FSM 的截断做准备 */
	fc_fsm = smgrexists(RelationGetSmgr(fc_rel), FSM_FORKNUM);
	if (fc_fsm)
	{
		fc_blocks[fc_nforks] = FreeSpaceMapPrepareTruncateRel(fc_rel, fc_nblocks);
		if (BlockNumberIsValid(fc_blocks[fc_nforks]))
		{
			fc_forks[fc_nforks] = FSM_FORKNUM;
			fc_old_blocks[fc_nforks] = smgrnblocks(fc_reln, FSM_FORKNUM);
			fc_nforks++;
			fc_need_fsm_vacuum = true;
		}
	}

	/* 如果存在，也为可见性映射的截断做准备 */
	fc_vm = smgrexists(RelationGetSmgr(fc_rel), VISIBILITYMAP_FORKNUM);
	if (fc_vm)
	{
		fc_blocks[fc_nforks] = visibilitymap_prepare_truncate(fc_rel, fc_nblocks);
		if (BlockNumberIsValid(fc_blocks[fc_nforks]))
		{
			fc_forks[fc_nforks] = VISIBILITYMAP_FORKNUM;
			fc_old_blocks[fc_nforks] = smgrnblocks(fc_reln, VISIBILITYMAP_FORKNUM);
			fc_nforks++;
		}
	}

	RelationPreTruncate(fc_rel);

	/*
	 * 以下代码可以通过两种方式与并发检查点交互。
	 *
	 * 首先，截断操作可能会删除检查点本应刷新的缓冲区。如果发生这种情况，那么在写入检查点记录之前，文件实际上必须在磁盘上被截断。否则，如果回复从该检查点开始，待截断的块可能仍然存在于磁盘上，但包含的内容比预期旧，这可能会导致重放失败。块可以完全不存在于磁盘上，但不能包含错误的内容。因此，在此代码执行期间，我们需要设置 DELAY_CHKPT_COMPLETE。
	 *
	 * 其次，下面对 smgrtruncate() 的调用将反过来调用 RegisterSyncRequest()。我们需要由该调用创建的同步请求在检查点完成之前被处理。CheckPointGuts() 将调用 ProcessSyncRequests()，但如果我们在那之后注册我们的同步请求，那么截断的 WAL 记录可能会在检查点记录之前出现，而实际的同步不会发生直到下一个检查点。为防止这种情况，我们需要在这里设置 DELAY_CHKPT_START。这样，如果 XLOG_SMGR_TRUNCATE 在并发检查点的重做指针之前，我们可以确保相应的同步请求在检查点完成之前被处理。
	 */
	Assert((MyProc->delayChkptFlags & (DELAY_CHKPT_START | DELAY_CHKPT_COMPLETE)) == 0);
	MyProc->delayChkptFlags |= DELAY_CHKPT_START | DELAY_CHKPT_COMPLETE;

	/*
	 * 我们首先记录截断的 WAL，然后在关键区域内进行截断。截断删除缓冲区，即使是脏的，然后截断磁盘文件。所有这些工作需要在释放锁之前完成，否则，缺少最近更改的旧版本页面将在磁盘上再次变得可访问。如果我们崩溃，将在崩溃恢复中重新尝试整个操作，但即使如此，我们也不能放弃，因为我们不希望备用的关系大小出现差异，从而破坏后续的重放或可见性不变性。关键区域还会抑制中断。
	 *
	 * （如果更改此代码，请参阅 pg_visibilitymap.c。）
	 */
	START_CRIT_SECTION();

	if (RelationNeedsWAL(fc_rel))
	{
		/*
		 * 制作一个 XLOG 条目，报告文件截断。
		 */
		XLogRecPtr	fc_lsn;
		xl_smgr_truncate fc_xlrec;

		fc_xlrec.blkno = fc_nblocks;
		fc_xlrec.rnode = fc_rel->rd_node;
		fc_xlrec.flags = SMGR_TRUNCATE_ALL;

		XLogBeginInsert();
		XLogRegisterData((char *) &fc_xlrec, sizeof(fc_xlrec));

		fc_lsn = XLogInsert(RM_SMGR_ID,
						 XLOG_SMGR_TRUNCATE | XLR_SPECIAL_REL_UPDATE);

		/*
		 * 刷新，因为否则主关系的截断可能会在 WAL 记录之前写入磁盘，FSM 或可见性映射的截断也是如此。如果我们在那段时间崩溃，我们将只剩下一个截断的堆，但 FSM 或可见性映射仍将包含不存在的堆页面的条目，备用也永远不会重放截断。
		 */
		XLogFlush(fc_lsn);
	}

	/*
	 * 这将首先从缓冲池中删除任何在截断完成后不应再存在的缓冲区，然后在磁盘上截断相应的文件。
	 */
	smgrtruncate2(RelationGetSmgr(fc_rel), fc_forks, fc_nforks, fc_old_blocks, fc_blocks);

	END_CRIT_SECTION();

	/* 我们已经完成了所有的关键工作，现在检查点是可以的。 */
	MyProc->delayChkptFlags &= ~(DELAY_CHKPT_START | DELAY_CHKPT_COMPLETE);

	/*
	 * 更新上层 FSM 页面以考虑截断。这很重要，因为刚刚截断的页面可能标记为全空，并且会优先被选择。
	 *
	 * 注意：在完成此操作之前，没有必要延迟检查点。
	 * 因为 FSM 没有被 WAL 记录，所以无论如何我们都必须准备好在崩溃后发生损坏的可能性。
	 */
	if (fc_need_fsm_vacuum)
		FreeSpaceMapVacuumRange(fc_rel, fc_nblocks, InvalidBlockNumber);
}

/*
 * RelationPreTruncate
 *		在物理截断之前执行 AM 独立的工作。
 *
 * 如果一个访问方法的 relation_nontransactional_truncate 没有调用 RelationTruncate()，则它必须在减小表大小之前调用此函数。
 */
void RelationPreTruncate(Relation fc_rel)
{
	PendingRelSync *fc_pending;

	if (!pendingSyncHash)
		return;

	fc_pending = hash_search(pendingSyncHash,
						  &(RelationGetSmgr(fc_rel)->smgr_rnode.node),
						  HASH_FIND, NULL);
	if (fc_pending)
		fc_pending->is_truncated = true;
}

/*
 * 按块复制分支的数据。
 *
 * 请注意，这要求共享缓冲区中没有脏数据。如果可能存在脏数据，调用者需要使用例如 FlushRelationBuffers(rel) 刷新它们。
 *
 * 还请注意，这通常通过如下语言调用：
 *		RelationCopyStorage(RelationGetSmgr(rel), ...);
 * 这仅安全，因为我们在这里只执行 smgr 和 WAL 操作。
 * 如果我们调用其他任何内容，relcache 刷新可能导致我们的 SMgrRelation 参数变成悬空指针。
 */
void RelationCopyStorage(SMgrRelation fc_src, SMgrRelation fc_dst,
					ForkNumber fc_forkNum, char fc_relpersistence)
{
	PGAlignedBlock fc_buf;
	Page		fc_page;
	bool		fc_use_wal;
	bool		fc_copying_initfork;
	BlockNumber fc_nblocks;
	BlockNumber fc_blkno;

	fc_page = (Page) fc_buf.data;

	/*
	 * 对于未记录的关系，初始化分支在许多方面必须与正常关系相同，变更需要进行 WAL 记录，并且需要同步到磁盘。
	 */
	fc_copying_initfork = fc_relpersistence == RELPERSISTENCE_UNLOGGED &&
		fc_forkNum == INIT_FORKNUM;

	
/*
	 * 只有在启用了WAL归档/流媒体并且这是一个永久关系时，我们才需要在WAL中记录被复制的数据。这与“RelationNeedsWAL(rel) || copying_initfork”给出的答案相同，因为我们知道当前操作创建了一个新的relfilenode。
	 */
	fc_use_wal = XLogIsNeeded() &&
		(fc_relpersistence == RELPERSISTENCE_PERMANENT || fc_copying_initfork);

	fc_nblocks = smgrnblocks(fc_src, fc_forkNum);

	for (fc_blkno = 0; fc_blkno < fc_nblocks; fc_blkno++)
	{
		/* 如果在复制数据期间收到了取消信号，则退出 */
		CHECK_FOR_INTERRUPTS();

		smgrread(fc_src, fc_forkNum, fc_blkno, fc_buf.data);

		if (!PageIsVerifiedExtended(fc_page, fc_blkno,
									PIV_LOG_WARNING | PIV_REPORT_STAT))
		{
			/*
			 * 为了防止意外，在调用ereport机制之前捕获文件路径。这可以防止由于例如errcontext回调导致的relcache刷新发生。
			 * （errcontext回调不应该冒任何风险，但有些人已知会忘记这个规则。）
			 */
			char	   *fc_relpath = relpathbackend(fc_src->smgr_rnode.node,
												 fc_src->smgr_rnode.backend,
												 fc_forkNum);

			ereport(ERROR,
					(errcode(ERRCODE_DATA_CORRUPTED),
					 errmsg("invalid page in block %u of relation %s",
							fc_blkno, fc_relpath)));
		}

		/*
		 * WAL记录复制的页面。不幸的是，我们不知道这是什么类型的页面，因此必须记录包括任何未使用空间在内的完整页面。
		 */
		if (fc_use_wal)
			log_newpage(&fc_dst->smgr_rnode.node, fc_forkNum, fc_blkno, fc_page, false);

		PageSetChecksumInplace(fc_page, fc_blkno);

		/*
		 * 现在写入页面。我们说skipFsync = true，因为没有必要让smgr为这一写入调度fsync；我们将在下面自己执行这个操作。
		 */
		smgrextend(fc_dst, fc_forkNum, fc_blkno, fc_buf.data, true);
	}

	/*
	 * 当我们WAL记录关系页面时，我们必须对它们进行fsync。原因是由于我们在共享缓冲区外进行复制，因此在复制过程中发生的CHECKPOINT无法将之前写入的数据刷新到磁盘（实际上它无法知道新的关系甚至存在）。后续的崩溃将从检查点重播WAL，因此将不会重播我们之前的WAL条目。如果我们在这里不对那些页面进行fsync，当崩溃发生时它们可能仍未写入磁盘。
	 */
	if (fc_use_wal || fc_copying_initfork)
		smgrimmedsync(fc_dst, fc_forkNum);
}

/*
 * RelFileNodeSkippingWAL
 *		检查一个BM_PERMANENT的relfilenode是否在使用WAL。
 *
 * 某些relfilenodes的变化不得写入WAL；请参见src/backend/access/transam/README中的“跳过新RelFileNode的WAL”。虽然从关系中可以有效地知道这一点，但此功能的目的是为了代码路径没有访问到关系。
 */
bool RelFileNodeSkippingWAL(RelFileNode fc_rnode)
{
	if (!pendingSyncHash ||
		hash_search(pendingSyncHash, &fc_rnode, HASH_FIND, NULL) == NULL)
		return false;

	return true;
}

/*
 * EstimatePendingSyncsSpace
 *		估算传递给并行工作进程的同步所需的空间。
 */
Size EstimatePendingSyncsSpace(void)
{
	long		fc_entries;

	fc_entries = pendingSyncHash ? hash_get_num_entries(pendingSyncHash) : 0;
	return mul_size(1 + fc_entries, sizeof(RelFileNode));
}

/*
 * SerializePendingSyncs
 *		为并行工作进程序列化同步。
 */
void SerializePendingSyncs(Size fc_maxSize, char *fc_startAddress)
{
	HTAB	   *fc_tmphash;
	HASHCTL		fc_ctl;
	HASH_SEQ_STATUS fc_scan;
	PendingRelSync *fc_sync;
	PendingRelDelete *fc_delete;
	RelFileNode *fc_src;
	RelFileNode *fc_dest = (RelFileNode *) fc_startAddress;

	if (!pendingSyncHash)
		goto terminate;

	/* 创建临时哈希以收集活动的relfilenodes */
	fc_ctl.keysize = sizeof(RelFileNode);
	fc_ctl.entrysize = sizeof(RelFileNode);
	fc_ctl.hcxt = CurrentMemoryContext;
	fc_tmphash = hash_create("tmp relfilenodes",
						  hash_get_num_entries(pendingSyncHash), &fc_ctl,
						  HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);

	/* 从待处理同步中收集所有rnodes */
	hash_seq_init(&fc_scan, pendingSyncHash);
	while ((fc_sync = (PendingRelSync *) hash_seq_search(&fc_scan)))
		(void) hash_search(fc_tmphash, &fc_sync->rnode, HASH_ENTER, NULL);

	/* 删除已删除的rnodes */
	for (fc_delete = pendingDeletes; fc_delete != NULL; fc_delete = fc_delete->next)
		if (fc_delete->atCommit)
			(void) hash_search(fc_tmphash, (void *) &fc_delete->relnode,
							   HASH_REMOVE, NULL);

	hash_seq_init(&fc_scan, fc_tmphash);
	while ((fc_src = (RelFileNode *) hash_seq_search(&fc_scan)))
		*fc_dest++ = *fc_src;

	hash_destroy(fc_tmphash);

terminate:
	MemSet(fc_dest, 0, sizeof(RelFileNode));
}

/*
 * RestorePendingSyncs
 *		恢复并行工作者中的同步。
 *
 * RelationNeedsWAL() 和 RelFileNodeSkippingWAL() 必须对并行工作者提供正确的答案。
 * 只有 smgrDoPendingSyncs() 在事务结束时读取 is_truncated 字段。因此，不要恢复它。
 */
void RestorePendingSyncs(char *fc_startAddress)
{
	RelFileNode *fc_rnode;

	Assert(pendingSyncHash == NULL);
	for (fc_rnode = (RelFileNode *) fc_startAddress; fc_rnode->relNode != 0; fc_rnode++)
		fc_AddPendingSync(fc_rnode);
}

/*
 *	smgrDoPendingDeletes() -- 在 xact 结束时处理关系删除。
 *
 * 在中止子事务时也会运行此函数；我们希望立即清理失败的子事务。
 *
 * 注意：可能我们被要求删除一个在任何分支上都没有物理存储的关系。
 * 特别是，可能我们正在清理一个旧的临时关系，RemovePgTempFiles 已经回收了物理存储。
 */
void smgrDoPendingDeletes(bool fc_isCommit)
{
	int			fc_nestLevel = GetCurrentTransactionNestLevel();
	PendingRelDelete *fc_pending;
	PendingRelDelete *fc_prev;
	PendingRelDelete *fc_next;
	int			fc_nrels = 0,
				fc_maxrels = 0;
	SMgrRelation *fc_srels = NULL;

	fc_prev = NULL;
	for (fc_pending = pendingDeletes; fc_pending != NULL; fc_pending = fc_next)
	{
		fc_next = fc_pending->next;
		if (fc_pending->nestLevel < fc_nestLevel)
		{
			/* 外层条目不应被处理 */
			fc_prev = fc_pending;
		}
		else
		{
			/* 首先解除链接列表条目，以便我们不在失败时重试 */
			if (fc_prev)
				fc_prev->next = fc_next;
			else
				pendingDeletes = fc_next;
			/* 如果需要则执行删除 */
			if (fc_pending->atCommit == fc_isCommit)
			{
				SMgrRelation fc_srel;

				fc_srel = smgropen(fc_pending->relnode, fc_pending->backend);

				/* 分配初始数组，或在需要时扩展它 */
				if (fc_maxrels == 0)
				{
					fc_maxrels = 8;
					fc_srels = palloc(sizeof(SMgrRelation) * fc_maxrels);
				}
				else if (fc_maxrels <= fc_nrels)
				{
					fc_maxrels *= 2;
					fc_srels = repalloc(fc_srels, sizeof(SMgrRelation) * fc_maxrels);
				}

				fc_srels[fc_nrels++] = fc_srel;
			}
			/* 必须显式自由列表条目 */
			pfree(fc_pending);
			/* prev 不变 */
		}
	}

	if (fc_nrels > 0)
	{
		smgrdounlinkall(fc_srels, fc_nrels, false);

		for (int fc_i = 0; fc_i < fc_nrels; fc_i++)
			smgrclose(fc_srels[fc_i]);

		pfree(fc_srels);
	}
}

/*
 *	smgrDoPendingSyncs() -- 在 xact 结束时处理关系同步。
 */
void smgrDoPendingSyncs(bool fc_isCommit, bool fc_isParallelWorker)
{
	PendingRelDelete *fc_pending;
	int			fc_nrels = 0,
				fc_maxrels = 0;
	SMgrRelation *fc_srels = NULL;
	HASH_SEQ_STATUS fc_scan;
	PendingRelSync *fc_pendingsync;

	Assert(GetCurrentTransactionNestLevel() == 1);

	if (!pendingSyncHash)
		return;					/* 没有关系需要同步 */

	/* 中止 -- 只需丢弃所有待处理的同步 */
	if (!fc_isCommit)
	{
		pendingSyncHash = NULL;
		return;
	}

	AssertPendingSyncs_RelationCache();

	/* 并行工作者 -- 只需丢弃所有待处理的同步 */
	if (fc_isParallelWorker)
	{
		pendingSyncHash = NULL;
		return;
	}

	/* 跳过 smgrDoPendingDeletes() 将删除的节点的同步。 */
	for (fc_pending = pendingDeletes; fc_pending != NULL; fc_pending = fc_pending->next)
		if (fc_pending->atCommit)
			(void) hash_search(pendingSyncHash, (void *) &fc_pending->relnode,
							   HASH_REMOVE, NULL);

	hash_seq_init(&fc_scan, pendingSyncHash);
	while ((fc_pendingsync = (PendingRelSync *) hash_seq_search(&fc_scan)))
	{
		ForkNumber	fc_fork;
		BlockNumber fc_nblocks[MAX_FORKNUM + 1];
		uint64		fc_total_blocks = 0;
		SMgrRelation fc_srel;

		fc_srel = smgropen(fc_pendingsync->rnode, InvalidBackendId);

		/*
		 * 我们为较小的关系发出新的 WAL 记录。
		 *
		 * 较小的 WAL 记录有可能与其他后端的 WAL 记录一起发出。
		 * 我们为小于某个阈值的文件发出 WAL 记录而不是同步，期望更快的提交。
		 * 阈值由 GUC wal_skip_threshold 定义。
		 */
		if (!fc_pendingsync->is_truncated)
		{
			for (fc_fork = 0; fc_fork <= MAX_FORKNUM; fc_fork++)
			{
				if (smgrexists(fc_srel, fc_fork))
				{
					BlockNumber fc_n = smgrnblocks(fc_srel, fc_fork);

					/* 我们不应该为无日志的关系到这里 */
					Assert(fc_fork != INIT_FORKNUM);
					fc_nblocks[fc_fork] = fc_n;
					fc_total_blocks += fc_n;
				}
				else
					fc_nblocks[fc_fork] = InvalidBlockNumber;
			}
		}

		/*
		 * 同步文件或发出其内容的 WAL 记录。
		 *
		 * 虽然如果文件足够小我们会发出 WAL 记录，但如果文件经历了截断，
		 * 则无论文件大小如何都要进行文件同步。因为如果在过去较长文件刷写时，
		 * 我们遗漏了文件的同步并发出了 WAL，会导致文件在崩溃恢复后跟随垃圾块。
		 * 你可能认为如果当前主分支比以前长，我们可以选择 WAL，
		 * 但有一种情况是主分支比以前长但 FSM 分支变短。
		 */
		if (fc_pendingsync->is_truncated ||
			fc_total_blocks >= wal_skip_threshold * (uint64) 1024 / BLCKSZ)
		{
			/* 分配初始数组，或在需要时扩展它 */
			if (fc_maxrels == 0)
			{
				fc_maxrels = 8;
				fc_srels = palloc(sizeof(SMgrRelation) * fc_maxrels);
			}
			else if (fc_maxrels <= fc_nrels)
			{
				fc_maxrels *= 2;
				fc_srels = repalloc(fc_srels, sizeof(SMgrRelation) * fc_maxrels);
			}

			fc_srels[fc_nrels++] = fc_srel;
		}
		else
		{
			/* 为所有块发出 WAL 记录。文件足够小。 */
			for (fc_fork = 0; fc_fork <= MAX_FORKNUM; fc_fork++)
			{
				int			fc_n = fc_nblocks[fc_fork];
				Relation	fc_rel;

				if (!BlockNumberIsValid(fc_n))
					continue;

				/*
				 * 为整个文件发出 WAL。不幸的是，我们不知道这是什么类型的页面，
				 * 所以我们必须记录整个页面，包括任何未使用的空间。
				 * ReadBufferExtended() 统计了一些 pgstat 事件；不幸的是，我们丢弃了这些事件。
				 */
				fc_rel = CreateFakeRelcacheEntry(fc_srel->smgr_rnode.node);
				log_newpage_range(fc_rel, fc_fork, 0, fc_n, false);
				FreeFakeRelcacheEntry(fc_rel);
			}
		}
	}

	pendingSyncHash = NULL;

	if (fc_nrels > 0)
	{
		smgrdosyncall(fc_srels, fc_nrels);
		pfree(fc_srels);
	}
}

/*
 * smgrGetPendingDeletes() -- 获取待删除的非临时关系列表。
 *
 * 返回值是计划终止的关系数量。
 * *ptr 被设置为指向一个新分配的 RelFileNodes 数组。
 * 如果没有关系需要删除，*ptr 被设置为 NULL。
 *
 * 返回的列表仅包含非临时关系。这是可以的，因为该列表仅在临时关系无关的上下文中使用：
 * 我们要么正在写入两阶段状态文件（触碰临时表的事务无法准备），
 * 要么正在写入 xlog（所有临时文件如果重新启动时会被清除，因此不需要 redo 也这样做）。
 *
 * 请注意，该列表不包括由上层事务计划终止的任何内容。
 */
int smgrGetPendingDeletes(bool fc_forCommit, RelFileNode **fc_ptr)
{
	int			fc_nestLevel = GetCurrentTransactionNestLevel();
	int			fc_nrels;
	RelFileNode *fc_rptr;
	PendingRelDelete *fc_pending;

	fc_nrels = 0;
	for (fc_pending = pendingDeletes; fc_pending != NULL; fc_pending = fc_pending->next)
	{
		if (fc_pending->nestLevel >= fc_nestLevel && fc_pending->atCommit == fc_forCommit
			&& fc_pending->backend == InvalidBackendId)
			fc_nrels++;
	}
	if (fc_nrels == 0)
	{
		*fc_ptr = NULL;
		return 0;
	}
	fc_rptr = (RelFileNode *) palloc(fc_nrels * sizeof(RelFileNode));
	*fc_ptr = fc_rptr;
	for (fc_pending = pendingDeletes; fc_pending != NULL; fc_pending = fc_pending->next)
	{
		if (fc_pending->nestLevel >= fc_nestLevel && fc_pending->atCommit == fc_forCommit
			&& fc_pending->backend == InvalidBackendId)
		{
			*fc_rptr = fc_pending->relnode;
			fc_rptr++;
		}
	}
	return fc_nrels;
}

/*
 *	PostPrepare_smgr -- 在成功预处理后清理
 *
 * 我们在这里需要做的是丢弃有关待处理关系删除的内存状态。
 * 这已经记录在 2PC 状态文件中，smgr 不再需要担心它。
 */
void PostPrepare_smgr(void)
{
	PendingRelDelete *fc_pending;
	PendingRelDelete *fc_next;

	for (fc_pending = pendingDeletes; fc_pending != NULL; fc_pending = fc_next)
	{
		fc_next = fc_pending->next;
		pendingDeletes = fc_next;
		/* 必须显式自由列表条目 */
		pfree(fc_pending);
	}
}


/*
 * AtSubCommit_smgr() --- 处理子事务提交。
 *
 * 将待删除列表中的所有项目重新分配给父事务。
 */
void AtSubCommit_smgr(void)
{
	int			fc_nestLevel = GetCurrentTransactionNestLevel();
	PendingRelDelete *fc_pending;

	for (fc_pending = pendingDeletes; fc_pending != NULL; fc_pending = fc_pending->next)
	{
		if (fc_pending->nestLevel >= fc_nestLevel)
			fc_pending->nestLevel = fc_nestLevel - 1;
	}
}

/*
 * AtSubAbort_smgr() --- 处理子事务中止。
 *
 * 删除创建的关系并忘记删除的关系。
 * 我们可以立即执行这些操作，因为我们知道此子事务不会提交。
 */
void AtSubAbort_smgr(void)
{
	smgrDoPendingDeletes(false);
}

void smgr_redo(XLogReaderState *fc_record)
{
	XLogRecPtr	fc_lsn = fc_record->EndRecPtr;
	uint8		fc_info = XLogRecGetInfo(fc_record) & ~XLR_INFO_MASK;

	/* 备份块在 smgr 记录中未使用 */
	Assert(!XLogRecHasAnyBlockRefs(fc_record));

	if (fc_info == XLOG_SMGR_CREATE)
	{
		xl_smgr_create *fc_xlrec = (xl_smgr_create *) XLogRecGetData(fc_record);
		SMgrRelation fc_reln;

		fc_reln = smgropen(fc_xlrec->rnode, InvalidBackendId);
		smgrcreate(fc_reln, fc_xlrec->forkNum, true);
	}
	else if (fc_info == XLOG_SMGR_TRUNCATE)
	{
		xl_smgr_truncate *fc_xlrec = (xl_smgr_truncate *) XLogRecGetData(fc_record);
		SMgrRelation fc_reln;
		Relation	fc_rel;
		ForkNumber	fc_forks[MAX_FORKNUM];
		BlockNumber fc_blocks[MAX_FORKNUM];
		BlockNumber fc_old_blocks[MAX_FORKNUM];
		int			fc_nforks = 0;
		bool		fc_need_fsm_vacuum = false;

		fc_reln = smgropen(fc_xlrec->rnode, InvalidBackendId);

		/*
		 * 如果关系不存在则强制创建关系（这表明它在 WAL 序列的某个地方被删除）。
		 * 与 XLogReadBufferForRedo 一样，我们倾向于重新创建关系并尽可能重放日志，
		 * 直到看到删除为止。
		 */
		smgrcreate(fc_reln, MAIN_FORKNUM, true);

		
/*
		 * 在我们执行截断之前，更新最小恢复点以覆盖这个 WAL 记录。
		 * 一旦关系被截断，就无法恢复。缓冲区管理器为
		 * 关系文件的正常更新强制执行 WAL 优先规则，因此最小恢复
		 * 点总是在相应的数据文件更改刷新到磁盘之前进行更新。
		 * 我们必须在这里手动执行相同的操作。
		 *
		 * 在截断之前这样做意味着如果截断由于某种原因失败，
		 * 即使在重启后也无法启动系统，直到修复底层情况以使
		 * 截断能够成功。或者，我们可以在截断后更新最小恢复点，
		 * 但那样会留下一个小窗口，可能会违反 WAL 优先规则。
		 */
		XLogFlush(fc_lsn);

		/* 准备主分支的截断 */
		if ((fc_xlrec->flags & SMGR_TRUNCATE_HEAP) != 0)
		{
			fc_forks[fc_nforks] = MAIN_FORKNUM;
			fc_old_blocks[fc_nforks] = smgrnblocks(fc_reln, MAIN_FORKNUM);
			fc_blocks[fc_nforks] = fc_xlrec->blkno;
			fc_nforks++;

			/* 还要告诉 xlogutils.c 这件事 */
			XLogTruncateRelation(fc_xlrec->rnode, MAIN_FORKNUM, fc_xlrec->blkno);
		}

		/* 也准备截断 FSM 和 VM */
		fc_rel = CreateFakeRelcacheEntry(fc_xlrec->rnode);

		if ((fc_xlrec->flags & SMGR_TRUNCATE_FSM) != 0 &&
			smgrexists(fc_reln, FSM_FORKNUM))
		{
			fc_blocks[fc_nforks] = FreeSpaceMapPrepareTruncateRel(fc_rel, fc_xlrec->blkno);
			if (BlockNumberIsValid(fc_blocks[fc_nforks]))
			{
				fc_forks[fc_nforks] = FSM_FORKNUM;
				fc_old_blocks[fc_nforks] = smgrnblocks(fc_reln, FSM_FORKNUM);
				fc_nforks++;
				fc_need_fsm_vacuum = true;
			}
		}
		if ((fc_xlrec->flags & SMGR_TRUNCATE_VM) != 0 &&
			smgrexists(fc_reln, VISIBILITYMAP_FORKNUM))
		{
			fc_blocks[fc_nforks] = visibilitymap_prepare_truncate(fc_rel, fc_xlrec->blkno);
			if (BlockNumberIsValid(fc_blocks[fc_nforks]))
			{
				fc_forks[fc_nforks] = VISIBILITYMAP_FORKNUM;
				fc_old_blocks[fc_nforks] = smgrnblocks(fc_reln, VISIBILITYMAP_FORKNUM);
				fc_nforks++;
			}
		}

		/* 进行实际的关系分支截断工作 */
		if (fc_nforks > 0)
		{
			START_CRIT_SECTION();
			smgrtruncate2(fc_reln, fc_forks, fc_nforks, fc_old_blocks, fc_blocks);
			END_CRIT_SECTION();
		}

		/*
		 * 更新上级 FSM 页以考虑截断。这很重要，因为刚刚被截断的
		 * 页面可能被标记为全空，并且会被优先选择。
		 */
		if (fc_need_fsm_vacuum)
			FreeSpaceMapVacuumRange(fc_rel, fc_xlrec->blkno,
									InvalidBlockNumber);

		FreeFakeRelcacheEntry(fc_rel);
	}
	else
		elog(PANIC, "smgr_redo: unknown op code %u", fc_info);
}
